
import unittest

class MyTime(object):
    def __init__(self,hour,minute):
        self._hour = hour
        self._minute = minute
        
    def __str__(self):
        return "%d:%d"%(self._hour,self._minute)
    
    def __add__(self,other):
        return self.__class__(self._hour + other._hour,self._minute + other._minute)
    
    def __iadd__(self,other):
        """
        in place add
        """
        self._hour += other._hour
        self._minute += other._minute
        return self
    
    def __eq__(self,other):
        if other is None:
            return False
        elif self is other:
            return True
        elif isinstance(other,self.__class__):
            return self._hour == other._hour and self._minute == other._minute
        else:
            return False
        
    def __ne__(self,other):
        return not self == other
    
    def __iter__(self):
        def generator():
            yield self._hour
            yield self._minute
        return generator()
    
    def __nonzero__(self):
        """
        let this object can be used as boolean
        """
        return self._hour or self._minute
    
    def __cmp__(self,other):
        if self._hour > other._hour:
            return 1
        elif self._hour == other._hour:
            return self._minute - other._minute
        else:
            return -1
        
    def __getattr__(self,attr):
        """
        this method is called only when attr is not found in current instance
        """
        if attr == "second":
            return 0
        else:
            raise AttributeError("not recognized attribute: %s"%attr)
        
    def __getitem__(self,index):
        if index == 0:
            return self._hour
        elif index == 1:
            return self._minute
        else:
            raise IndexError("Valid Index Only 0 and 1")
        
    def __call__(self):
        """
        it can has argument or not, both are OK
        because Python doesn't support overload, method with same name but different arguments will still overwrite previous one
        """
        return self._hour,self._minute
    
class TestCustomClassSample(unittest.TestCase):
    def test_str(self):
        self.assertEqual("11:21",str(MyTime(11,21)))
        
    def test_equal(self):
        tm1 = MyTime(11,26)
        tm2 = MyTime(11,26)
        
        self.assertNotEqual(1,tm1)
        
        self.assertTrue(tm1 is not tm2)
        self.assertEqual(tm1,tm2)
        
    def test_add(self):
        tm1 = MyTime(11,26)
        tm2 = MyTime(11,30)
        
        expected = MyTime(22,56)
        self.assertEqual(expected,tm1 + tm2)
        
        tm1 += tm2
        self.assertEqual(expected,tm1)
        
    def test_iterate(self):
        tm = MyTime(11,48)
        
        time_numbers = []
        for num in tm:
            time_numbers.append(num)
        
        self.assertEqual([11,48],time_numbers)
        
    def check_bool(self,tm):
        if tm:
            return True
        else: 
            return False
        
    def test_bool(self):
        self.assertTrue(self.check_bool(MyTime(1,0)))
        self.assertFalse(self.check_bool(MyTime(0,0)))
        
    def test_compare(self):
        tm1 = MyTime(11,50)
        tm2 = MyTime(11,40)
        self.assertTrue(tm1 > tm2)
        
        tm1 = MyTime(11,50)
        tm2 = MyTime(12,5)
        self.assertTrue(tm1 < tm2)
        
    def test_custom_getattr(self):
        tm1 = MyTime(11,50)
        self.assertEqual(0,tm1.second)
        
        self.assertRaises(AttributeError,lambda:  tm1.day) 
        
    def test_getitem(self):
        tm = MyTime(14,30)
        self.assertEqual(14,tm[0])
        self.assertEqual(30,tm[1])
        self.assertRaises(IndexError,lambda : tm[3])
        
    def test_callable(self):
        tm = MyTime(3,45)
        self.assertEqual((3,45),tm())
        
class MyCustomList(object):
    """
    demonstrate how to write a wrapper to add extra function to built-in type
    """
    def __init__(self,atype):
        self.__list = []
        self.__type = atype
        
    def append(self,data):
        if isinstance(data,self.__type):
            self.__list.append(data)
        else:
            raise TypeError("this list only limited to type[%s]"%(self.__type.__name__))
        
    def __str__(self): 
        return str(self.__list)
    
    def tolist(self):
        return self.__list
    
    def __getattr__(self,attr):
        """
        by using "__getattr__", there is no need to add wrapper API for each inside API
        """
        return getattr(self.__list,attr)
    
class TestBuiltInWrapper(unittest.TestCase):
    def test_limitation(self):
        intlist = MyCustomList(int)
        intlist.append(5)
        self.assertRaises(TypeError, lambda : intlist.append("cheka") )
        
    def test_builtin_api(self):
        """
        by delegate "__getattr__" to inside object, APIs of builtin type is still valid
        """
        intlist = MyCustomList(int)
        intlist.append(5)
        intlist.append(1)
        intlist.append(2)
        intlist.append(10)
        
        self.assertEqual(1,intlist.pop(1) )
        self.assertEqual(1,intlist.index(2) )
        

class TestClassSpecialMethods(unittest.TestCase):
    """
    test using speicial methods in object, to custom its behaviours, such as comparing, string,......,etc
    """
    # ========================================== #
    # helper objects and methods
    # ========================================== #
    class Person(object):
        def __init__(self,ssn,name):
            self._ssn = ssn
            self._name = name
            
        def __str__(self):
            return "ssn=%d,name=%s"%(self._ssn,self._name)
        
        def __eq__(self,rhs):
            if (isinstance(rhs,TestClassSpecialMethods.Person)):
                if (self is rhs):
                    return True
                else:
                    return (self._name == rhs._name) and (self._ssn == rhs._ssn)
            else:
                return False
            
        # must provide "!=" yourself, "!=" will not be automatically be override since you have override "=="
        def __ne__(self,rhs):
            return not self == rhs
            
    # ========================================== #
    # test
    # ========================================== #

    def test_override_string(self):
        person = TestClassSpecialMethods.Person(1,"cheka")
        self.assertEqual("ssn=1,name=cheka",str(person))
        
    def test_override_equal(self):
        person1 = TestClassSpecialMethods.Person(1,"cheka")
        
        # test equal between different type        
        self.assertFalse(person1 == 1)
        self.assertTrue(person1 != 1) # __ne__ has been override
        
        # test equal with itself
        self.assertEqual(person1,person1)
        
        # test equal with object having same content
        person2 = TestClassSpecialMethods.Person(1,"cheka")
        self.assertEqual(person1,person2)
        self.assertTrue(person1 is not person2)
        
        # test unequality
        person2._name = "new name"
        self.assertNotEqual(person1,person2)
        self.assertTrue(person1 != person2)
        
class TestCustomObjWithSpecialMethods(unittest.TestCase):
    """
    check that if custom object implement some special methods
    their behaviours are changed in the containers
    """
    class ObjNoSpecialMethod(object):
        def __init__(self,identifier):
            self._identifier = identifier
            
    class ObjWithSpecialMethod(ObjNoSpecialMethod):
        
        def __init__(self,identifier):
            TestCustomObjWithSpecialMethods.ObjNoSpecialMethod.__init__(self,identifier)
        
        def __eq__(self,rhs):
            if self is rhs:
                return True
            
            if isinstance(rhs,TestCustomObjWithSpecialMethods.ObjWithSpecialMethod):
                return self._identifier == rhs._identifier
            else:
                return False
            
        def __ne__(self,rhs):
            return not (self == rhs)
        
    # =================================================== #
    def test_in_no_specialmethod(self):
        """
        if not override "__eq__", "in" can only find same object, other than different object with same content
        """
        obj1 = TestCustomObjWithSpecialMethods.ObjNoSpecialMethod(1)
        alist = [obj1]
        self.assertTrue(obj1 in alist)
        
        obj2 = TestCustomObjWithSpecialMethods.ObjNoSpecialMethod(1)
        self.assertTrue(obj2 not in alist)
        
    def test_in_with_specialmethod(self):
        """
        if override "__eq__", "in" can find different object but with same content
        """
        alist = [TestCustomObjWithSpecialMethods.ObjWithSpecialMethod(100)]
        
        obj2 = TestCustomObjWithSpecialMethods.ObjWithSpecialMethod(100)
        self.assertTrue(obj2 in alist)
    
        
    def test_collection_equal_defequal(self):
        """
        check equality of collection based on reference equality of each item
        """
        list1 = [TestCustomObjWithSpecialMethods.ObjNoSpecialMethod(1)]
        list2 = [TestCustomObjWithSpecialMethods.ObjNoSpecialMethod(1)]
        self.assertNotEqual(list1,list2)
        
    def test_collection_equal_overrideequal(self):
        """
        since item's __eq__ has been overriden to check their content other than refernce
        then collection can also automatically check equality based on content other than reference
        """
        list1 = [TestCustomObjWithSpecialMethods.ObjWithSpecialMethod(1)]
        list2 = [TestCustomObjWithSpecialMethods.ObjWithSpecialMethod(1)]
        
        self.assertTrue(list1 is not list2)
        self.assertTrue(list1[0] is not list2[0])
        self.assertEqual(list1,list2)
        

        
# ------------------------------- Template Pattern
class TemplateBase(object):
    def template_method(self):
        return self.sub_method1() + self.sub_method2()
    
    def sub_method1(self):
        raise NotImplementedError("must override in derived class")
    
    def sub_method2(self):
        raise NotImplementedError("must override in derived class")
    
    def sub_method3(self):
        return 5
    
class DerivedForInt(TemplateBase):
    def sub_method1(self): return 1 
    def sub_method2(self): return 2 
    
    def sub_method3(self):
        return 2 * super(DerivedForInt,self).sub_method3() 
    
class DerivedForString(TemplateBase):
    def sub_method1(self): return "cheka"
    def sub_method2(self): return "stasi"
# ------------------------------- End Template Pattern
        
class TestPolymorphism(unittest.TestCase):
    # =================== helper class definition
    class Grandpa(object) : pass
    class Father(Grandpa) : pass
    class Son(Father) : pass
        
    def test_template_pattern(self):
        int_product = DerivedForInt()
        self.assertEqual(3,int_product.template_method())
        
        string_product = DerivedForString()
        self.assertEqual("chekastasi",string_product.template_method())
        
    def test_call_super_method(self):
        obj = DerivedForString()
        self.assertEqual(5,obj.sub_method3()) # original base method
        
        obj = DerivedForInt()
        self.assertEqual(10,obj.sub_method3()) # override base method, but call base version inside it
        
    def test_issubclass(self):
        self.assertTrue(issubclass(TestPolymorphism.Son,TestPolymorphism.Father))
        self.assertTrue(issubclass(TestPolymorphism.Father,TestPolymorphism.Grandpa))
        # can be used to judge not-direct inheritance
        self.assertTrue(issubclass(TestPolymorphism.Son,TestPolymorphism.Grandpa))
        
    def test_isinstance(self):
        agrandpa = TestPolymorphism.Grandpa()
        afather = TestPolymorphism.Father()
        ason = TestPolymorphism.Son()
        
        # can be used to test super class
        self.assertTrue(isinstance(ason,TestPolymorphism.Father))
        self.assertTrue(isinstance(ason,TestPolymorphism.Grandpa))
        

if __name__ == "__main__":
    unittest.main()